多进程 or 多线程?

引入定义

现如今随着数据及任务复杂性,如果需要我们充分利用资源,提高处理效率,这时候我们有如下三种处理方案:

  • 多进程(多个进程同时处理多个任务)
  • 多线程(在一个进程内开启多个线程同时处理多个任务)
  • 多进程 + 多线程(使用多个进程,同时在多个进程下启动多个线程,但该方法往往比较复杂,使用较少)

那我们应该如何选择处理方案呢?在我们选择多进程还是多线程之前,我们先来了解一下进程和线程有什么特点,有什么区别?当我们了解其特点之后,便会了解我们在不同场景下应该如何选择了。这里首先用一个我看到的一个很形象的例子来解释进程与线程那些枯燥的定义。

进程 = 火车 | 线程 = 车厢

最少需要一个火车出发去解决问题(资源调度和分配的的基本单位)

一个火车可以携带多个车厢完成任务(一个进程可以分成多个线程,线程是最小执行和调度单位)

不同火车之间很难直接通信(不同进程之间不直接共享数据)

同列火车分享数据很容易(同一进程下的线程共享数据)

不同火车之间不会相互影响,但一个车厢着火,可能会导致火车着火(一个进程的问题不会影响到其他进程,但一个进程中的线程崩溃,可能导致该进程崩溃)

火车在不同轨道上,而同一火车的车厢不能在不同轨道上(一个线程不能独立运行)

火车的洗手间在使用时可以上锁,防止别人使用(加锁 Lock)

火车的餐厅有容纳人口数,超过则需等待(加信号量 Semaphore)

Python GIL 锁

上述是对概念的理解,下面就来解释一下我们常用编程脚本的特性。Python 作为上世纪出现的脚本语言,最早为了解决多线程共享内存的数据安全问题,引入了 GIL(Global Interpreter Lock)机制,即全局解释器。

GIL 的规定是,在一个进程中每次只能由一个线程在 python 解释器中运行(但这不代表多线程就不存在锁和同步),这可以看作线程运行的“通行证”,每当一个线程要运行时,必须先取得该“通行证”,接着在超时或者 I/O 时释放 GIL,让其他线程竞争,如此反复。正因如此,python 的多线程往往受到诟病,即 python 的多线程是假的,的确如此,python 多线程只能做到并发,而并不能做到真正的并行。GIL 由于每个进程有一个,因此是不会影响到多进程的。

既然 GIL 导致不能并行,那么为什么不删除 GIL 呢?

习惯 + 编程简单。单核时代,没有多进程、只有多线程,python 为了方便多线程编程,便引入了 GIL 锁,不是去增加活跃的线程数量,相反把活跃线程数减少,以降低 CPU 竞争从而达到提高程序性能的效果,同时保证内存安全,于是应用广泛。但当多核出现,并且在现在共享内存相关的 ⑴ 高频内存共享 ⑵ 大内存共享场景下,发现 GIL 这种实现对性能的影响似乎已经超过其所带来的安全性,但该方法编程的项目和代码量已经十分巨大,“牵一发而动全身”,并且人们已经习惯于该方法,于是 GIL 问题迟迟没有得到改善。

多进程 VS 多线程

既然进程每次只有一个线程在运行,那么我们就直接选择多进程就好啦,还犹豫什么?但回顾一下我们最开始提到的多进程的数据是不共享的,同时每个 CPU 上面只能运行一个进程,因此我们可以将任务简单分为 I/O 密集型和 CPU 密集型。

  • **CPU 密集型,选择多进程。**当大多数时间要花在 CPU 计算上时,两个 CPU 肯定比一个 CPU 跑的快,因此选择多进程是个好的选择。
  • **I/O 密集型,选择多线程。**当程序大多数时间花在 I/O 交互时,一个或者多个 CPU 差别不大,反而线程调度的开销要小于进程间数据通信,因此多线程是个好的选择。

需要注意的是,由于 GIL 的存在,多线程的运行时长可能比我们单线程的运行时长,因此在最终选择是,还是要结合任务复杂度,不能为了多线程而多线程,有时可能事倍功半。

其他

python 多线程使用 threading 库;

python 多进程使用 multiprocessing 库(往往运行代码内部程序,例如自定义方法等)和 subprocess 库(往往执行程序外部指令,例如 cmd 命令等)